/**
 * @(#)TaskList.java        1.0 2/15/12
 * 
 * Copyright (c) 2012 public static droid main
 * Georgia Institute of Technology, Atlanta, GA
 * All rights reserved.
 */

package edu.gatech.oad.publicstaticdroidmain.taskmanager.model;

import java.util.Date; 
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.Set;

import edu.gatech.oad.publicstaticdroidmain.taskmanager.model.Task.Recurrence;
import edu.gatech.oad.publicstaticdroidmain.taskmanager.model.Task.TaskAttribute;

/**
 * A data structure that holds Tasks.  Contains facilities for filtering
 * and searching.
 * 
 * @version 1.0 Feb 12 2012
 * @author Raheel Merali
 */
public class TaskList implements Iterable<Task> {
    /* In this implementation, Tasks are grouped into categories.  A
     * category is a String.
     * 
     * The Tasks in a particular category are stored in a HashMap (Tasks 
     * are hashed with their IDs).  The HashMaps that represent each category 
     * are then stored in another HashMap (categories are hashed with their 
     * names).
     */
    
    /* The message that is displayed if a Task has no assigned location */
    private static final String NO_LOCATION_MESSAGE = "NONE";
    
    /* The messages that are displayed to represent a Task's completion 
     * status if to2DArray is called */
    private static final String COMPLETED_MESSAGE = "Completed";
    private static final String NOT_COMPLETED_MESSAGE = "Not Completed";
    
    /* The Maps in this Map contain the Tasks in a specific category. */
    private Map<String, Map<Long, Task>> categoryMaps;
    
    /**
     * Creates an empty TaskList.
     * 
     */
    public TaskList() {
        categoryMaps = new HashMap<String, Map<Long, Task>>();
    }
    
    
    /**
     * Adds the specified Task to the specified category in this list.
     *
     * @param category the category in which to place the Task
     * @param toAdd the Task to add
     */
    public void add(String category, Task toAdd) {
        Map<Long, Task> thisCategory = categoryMaps.get(category);
        thisCategory.put(toAdd.getId(), toAdd);
    }
    
    /**
     * Removes the specified Task from this list if present.  If the specified 
     * Task exists in multiple categories, it will be removed from all of those
     * categories.
     *
     * @param toRemove the Task to remove
     * @return the removed Task or null if the removal failed
     */
    public Task remove(Task toRemove) {
        return remove(toRemove.getId());
    }
    
    /**
     * Removes the Task with the specified ID from this list if present.  
     * If a Task with the specified ID exists in multiple categories, it 
     * will be removed from all of those categories.
     *
     * @param toRemove the Task to remove
     * @return the removed Task or null if the removal failed
     */
    public Task remove(long id) {
        Task removedTask = null;
        
        Collection<Map<Long, Task>> allCategories = categoryMaps.values();
        
        for (Map<Long, Task> category : allCategories) {
            Task resultOfRemoval = category.remove(id);
            
            if (resultOfRemoval != null && removedTask == null) {
        	removedTask = resultOfRemoval;
            }
        }
        
        return removedTask;
    }
    
    /**
     * Returns the Task with the specified ID from this list, or null the
     * list does not contain a Task with the specified ID.
     *
     * @param id the ID of the task to retrieve
     * @return the Task with the specified ID, or null if no Task with the 
     * 		specified ID exists in this list
     */
    public Task get(long id) {
        Task requestedTask = null;
        
        Collection<Map<Long, Task>> allCategories = categoryMaps.values();
        
        for (Map<Long, Task> category : allCategories) {
            requestedTask = category.get(id);
            if (requestedTask != null) {
                break;
            }
        }
        
        return requestedTask;
    }
    
    /**
     * Returns the number of Tasks in this list.
     *
     * @return the number of Tasks in this list.
     */
    public int size() {
        int size = 0;
        
        Collection<Map<Long, Task>> allCategories = categoryMaps.values();
        
        for (Map<Long, Task> category : allCategories) {
            size += category.size();
        }
        
        return size;
    }
    
    /**
     * Returns true if this list contains no Tasks.
     *
     * @return true if this list contains no Tasks; false otherwise
     */
    public boolean isEmpty() {
        if (categoryMaps.size() == 0) {
            return true;
        }

        Collection<Map<Long, Task>> allCategories = categoryMaps.values();
        
        for (Map<Long, Task> category : allCategories) {
            if (!category.isEmpty()) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * Returns a String array containing every category name in this list.
     *
     * @return an array containing every category name in this list.
     */
    public String[] getCategories() {
	/* keySet() returns an Object array, so the array it generates needs 
	 * to be copied to a String array */
        Object[] categoriesObjArray = categoryMaps.keySet().toArray();
        int numCategories = categoriesObjArray.length;
        
        String[] categoriesStrArray = new String[numCategories];
        for (int i = 0; i < numCategories; i++) {
            categoriesStrArray[i] = categoriesObjArray[i].toString();
        }
        
        return categoriesStrArray;
    }
    
    /**
     * Adds the specified category to this list if it is not already present.
     *
     * @param categoryName the name of the category to add
     * @return true if the category was successfully added; false if the category
     * 		was already present in this list
     */
    public boolean addCategory(String categoryName) {
        boolean successfullyAdded = false;
        
        if (!categoryMaps.containsKey(categoryName)) {
            Map<Long, Task> newCategoryMap = new HashMap<Long, Task>();
            categoryMaps.put(categoryName, newCategoryMap);
            
            successfullyAdded = true;
        }
        
        return successfullyAdded;
    }
    
    /**
     * Removes the specified category (if present) and all of the Tasks 
     * in that category from this list.
     *
     * @param categoryName the name of the category to remove
     * @return the name of the removed category if the removal was successful; null
     * 		if the specified category was not found
     */
    public String removeCategory(String categoryName) {
        String removedCategoryName = null;
        if (categoryMaps.remove(categoryName) != null) {
            removedCategoryName = categoryName;
        }
        
        return removedCategoryName;
    }
    
    
    /**
     * Changes the name of the specified category to the specified new name.
     *
     * @param oldName the name of the category whose name should change
     * @param newName the new name for the category
     * @return true if the name change succeeded; false if the specified category
     * 		was not found
     */
    public boolean editCategoryName(String oldName, String newName) {
        boolean successfullyEdited = false;
        
        Map<Long, Task> thisCategoryMap = categoryMaps.remove(oldName);
        if (thisCategoryMap != null) {
            categoryMaps.put(newName, thisCategoryMap);
            successfullyEdited = true;
        }
        
        return successfullyEdited;
    }
    
    /**
     * Determines whether the specified category exists in this list.
     *
     * @param categoryName the name of the category
     * @return true if the specified category exists in this list; false
     * 		otherwise
     */
    public boolean categoryExists(String categoryName) {
        return categoryMaps.containsKey(categoryName);
    }
    
    /**
     * Returns a new TaskList containing only the Tasks in the specified category
     * (no other categories will be included).  The new list is not backed by this 
     * list, so changes to it will not be reflected in this list and vice versa.  
     * 
     * If the specified category does not exist, the returned list will be empty
     * (it will contain no categories and no Tasks).  If the specified category 
     * does exist but contains no Tasks, the returned list will contain the specified 
     * category but will not have any Tasks.
     *
     * @param categoryToFilterBy the name of the category to filter by
     * @return a new TaskList containing only the Tasks in the specified category
     */
    public TaskList filterByCategory(String categoryToFilterBy) { 
	TaskList filteredList = new TaskList();
	
	Map<Long, Task> thisCategoryMap = categoryMaps.get(categoryToFilterBy);
	
	if (thisCategoryMap != null) {
	    filteredList.addCategory(categoryToFilterBy);
	    
	    Collection<Task> tasksInThisCategory = thisCategoryMap.values();
	    for (Task t : tasksInThisCategory) {
		filteredList.add(categoryToFilterBy, t);
	    }
	}
	
	return filteredList;
    }

    /**
     * Returns a new TaskList containing only the Tasks in this list whose
     * due dates are within the specified range (inclusive).  The new list
     * is not backed by this list, so changes in it will not be reflected
     * in this list and vice versa.
     * 
     * The returned list will contain the same categories as this list, but
     * will only contain the Tasks in those categories that fall within the 
     * specified date range.  If no Tasks in a category fall within the
     * specified range, that category will exist but will be empty.
     * 
     * @param start the starting date
     * @param end the ending date
     * @return a new TaskList containing only the Tasks in the list whose due
     * 		dates are within the specified range (inclusive)
     */
    public TaskList filterByDueDate(Date start, Date end) { 
	TaskList filteredList = new TaskList();
	
	/* Entries are K-V pairs - the keys are the category names and the 
	 * values are the Maps containing the Tasks in those categories. */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();
	
	for (Entry<String, Map<Long, Task>> entry : categoryEntries) {
	    String categoryName = entry.getKey();
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();
	    
	    filteredList.addCategory(categoryName);
	    
	    for (Task t : tasksInThisCategory) {
		Date thisTaskDueDate = t.getDueDate();
		boolean withinRange = ( thisTaskDueDate.equals(start) || 
					thisTaskDueDate.after(start) ) && 
					( thisTaskDueDate.equals(end) || 
					thisTaskDueDate.before(end) );
		
		if (withinRange) {
		    filteredList.add(categoryName, t);
		}
	    }
	    
	}
	
	return filteredList;
    }
  
    
    /**
     * Returns a new TaskList containing only the Tasks in this list whose
     * recurrences are equal to the specified recurrence.  The new list
     * is not backed by this list, so changes in it will not be reflected
     * in this list and vice versa.
     * 
     * The returned list will contain the same categories as this list, but
     * will only contain the Tasks in those categories that have the specified
     * recurrence.  If no Tasks in a category have the specified recurrence, 
     * that category will exist but will be empty.
     * 
     * @param recurrence the Recurrence to filter by
     * @return a new TaskList containing only the Tasks in the list whose due
     * 		Recurrences are equal to the specified Recurrence
     */
    public TaskList filterByRecurrence(Task.Recurrence recurrence) { 
	TaskList filteredList = new TaskList();
	
	/* Entries are K-V pairs - the keys are the category names and the 
	 * values are the Maps containing the Tasks in those categories. */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();
	
	for (Entry<String, Map<Long, Task>> entry : categoryEntries) {
	    String categoryName = entry.getKey();
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();
	    
	    filteredList.addCategory(categoryName);
	    
	    for (Task t : tasksInThisCategory) {
		Task.Recurrence thisTaskRecurrence = t.getRecurrence();
		
		if (thisTaskRecurrence.equals(recurrence)) {
		    filteredList.add(categoryName, t);
		}
	    }
	    
	}
	
	return filteredList;
    }
    
    /**
     * Returns a new TaskList containing only the Tasks in this list whose
     * latitudes are within the specified range (inclusive).  The new list
     * is not backed by this list, so changes in it will not be reflected
     * in this list and vice versa.
     * 
     * The returned list will contain the same categories as this list, but
     * will only contain the Tasks in those categories that fall within the 
     * specified latitude range.  If no Tasks in a category fall within the
     * specified range, that category will exist but will be empty.
     * 
     * @param start the lower bound for the latitude
     * @param end the upper bound for the latitude
     * @return a new TaskList containing only the Tasks in the list whose 
     * 		latitudes are within the specified range (inclusive)
     */
    public TaskList filterByLatitude(double startLatitude, double endLatitude) { 
	TaskList filteredList = new TaskList();

	/* Entries are K-V pairs - the keys are the category names and the 
	 * values are the Maps containing the Tasks in those categories. */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();

	for (Entry<String, Map<Long, Task>> entry : categoryEntries) {
	    String categoryName = entry.getKey();
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();

	    filteredList.addCategory(categoryName);

	    for (Task t : tasksInThisCategory) {
		double thisTaskLatitude = t.getLatitude();
		boolean withinRange = (thisTaskLatitude >= startLatitude) && 
					(thisTaskLatitude <= endLatitude);

		if (withinRange) {
		    filteredList.add(categoryName, t);
		}
	    }

	}

	return filteredList;
    }
    
    /**
     * Returns a new TaskList containing only the Tasks in this list whose
     * longitudes are within the specified range (inclusive).  The new list
     * is not backed by this list, so changes in it will not be reflected
     * in this list and vice versa.
     * 
     * The returned list will contain the same categories as this list, but
     * will only contain the Tasks in those categories that fall within the 
     * specified longitude range.  If no Tasks in a category fall within the
     * specified range, that category will exist but will be empty.
     * 
     * @param start the lower bound for the longitude
     * @param end the upper bound for the longitude
     * @return a new TaskList containing only the Tasks in the list whose 
     * 		longitudes are within the specified range (inclusive)
     */
    public TaskList filterByLongitude(double startLongitude, double endLongitude) { 
	TaskList filteredList = new TaskList();

	/* Entries are K-V pairs - the keys are the category names and the 
	 * values are the Maps containing the Tasks in those categories. */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();

	for (Entry<String, Map<Long, Task>> entry : categoryEntries) {
	    String categoryName = entry.getKey();
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();

	    filteredList.addCategory(categoryName);

	    for (Task t : tasksInThisCategory) {
		double thisTaskLongitude = t.getLongitude();
		boolean withinRange = (thisTaskLongitude >= startLongitude) && 
					(thisTaskLongitude <= endLongitude);

		if (withinRange) {
		    filteredList.add(categoryName, t);
		}
	    }

	}

	return filteredList;
    }
    
    /**
     * Returns a new TaskList containing only the Tasks with the specified
     * completion status.  The new list is not backed by this list, so changes 
     * in it will not be reflected in this list and vice versa.
     * 
     * The returned list will contain the same categories as this list, but
     * will only contain the Tasks in those categories that have the specified
     * completion status.  If no Tasks in a category have the specified completion
     * status, that category will exist but will be empty.
     * 
     * @param completionStatus the completion status to filter by
     * @return a new TaskList containing only the Tasks in this list
     * 		with the specified completion status
     */
    public TaskList filterByCompletionStatus(boolean completionStatus) {
	TaskList filteredList = new TaskList();

	/* Entries are K-V pairs - the keys are the category names and the 
	 * values are the Maps containing the Tasks in those categories. */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();

	for (Entry<String, Map<Long, Task>> entry : categoryEntries) {
	    String categoryName = entry.getKey();
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();

	    filteredList.addCategory(categoryName);

	    for (Task t : tasksInThisCategory) {
		if (t.isCompleted() == completionStatus) {
		    filteredList.add(categoryName, t);
		}
	    }

	}

	return filteredList;
    }
    
    /**
     * Returns a new TaskList containing only the Tasks in this list whose
     * names contain the specified text.  The algorithm used to search Task
     * names is not very sophisticated - it only performs a simple check to
     * determine if the specified text is exactly contained within the Task's
     * name.
     * 
     * The new list is not backed by this list, so changes in it will not be 
     * reflected in this list and vice versa.
     * 
     * The returned list will contain the same categories as this list, but
     * will only contain the Tasks in those categories whose names contain
     * the specified text.  If no Tasks in a category have names that contain
     * the specified text, that category will exist but will be empty.
     * 
     * @param text the String to search Task names for
     * @return a new TaskList containing only the Tasks in the list whose 
     * 		names contain the specified String
     */
    public TaskList searchTaskNames(String text) {
	TaskList searchedList = new TaskList();

	/* Entries are K-V pairs - the keys are the category names and the 
	 * values are the Maps containing the Tasks in those categories. */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();

	for (Entry<String, Map<Long, Task>> entry : categoryEntries) {
	    String categoryName = entry.getKey();
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();

	    searchedList.addCategory(categoryName);

	    for (Task t : tasksInThisCategory) {
		/* Add this Task to the searched list if its name
		 * contains the specified text. */
		if (t.getName().contains(text)) {
		    searchedList.add(categoryName, t);
		}
	    }

	}

	return searchedList;
    }
    
    /**
     * Returns a new TaskList containing only the Tasks in this list whose
     * descriptions contain the specified text.  The algorithm used to search 
     * Task descriptions is not very sophisticated - it only performs a simple 
     * check to determine if the specified text is exactly contained within the 
     * Task's description.
     * 
     * The new list is not backed by this list, so changes in it will not be 
     * reflected in this list and vice versa.
     * 
     * The returned list will contain the same categories as this list, but
     * will only contain the Tasks in those categories whose descriptions 
     * contain the specified text.  If no Tasks in a category have descriptions 
     * that contain the specified text, that category will exist but will be 
     * empty.
     * 
     * @param text the String to search Task descriptions for
     * @return a new TaskList containing only the Tasks in the list whose 
     * 		descriptions contain the specified String
     */
    public TaskList searchTaskDescriptions(String text) {
	TaskList searchedList = new TaskList();

	/* Entries are K-V pairs - the keys are the category names and the 
	 * values are the Maps containing the Tasks in those categories. */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();

	for (Entry<String, Map<Long, Task>> entry : categoryEntries) {
	    String categoryName = entry.getKey();
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();

	    searchedList.addCategory(categoryName);

	    for (Task t : tasksInThisCategory) {
		/* Add this Task to the searched list if its name
		 * contains the specified text. */
		if (t.getDescription().contains(text)) {
		    searchedList.add(categoryName, t);
		}
	    }

	}

	return searchedList;
    }
    
    /**
     * Returns the category that the specified Task is in.
     *
     * @param t the Task whose category should be determined
     * @return the category the Task is in; null if the specified Task does
     * 		not exist in this TaskList
     */
    private String getCategory(Task t) {
	String category = null;
	
	Set<Entry<String, Map<Long, Task>>> categories = categoryMaps.entrySet();
	for (Entry<String, Map<Long, Task>> thisCategory : categories) {
	    Map<Long, Task> thisCategoryMap = thisCategory.getValue();
	    if (thisCategoryMap.containsValue(t)) {
		category = thisCategory.getKey();
		break;
	    }
	}
	
	return category;
    }
    
    
    /**
     * Creates an array containing the specified Task's data (the data included
     * is determined by the specified attributes).
     *
     * @param id the id of the Task whose data is to be retrieved
     * @param attributes the attributes of the Task that should be included
     * 
     * @return a String array containing the requested data
     */
    public String[] getTaskData(long id, TaskAttribute[] attributes) {
	Task t = get(id);
	
	String[] data = null;
	
	if (t != null) {
	    data = new String[attributes.length];
	    
	    int i = 0;
	    
	    for (TaskAttribute attribute : attributes) {
		    String whatToInsert = "";

		    switch(attribute) {
		    	case CATEGORY:
		    	    whatToInsert = getCategory(t);
		    	    break;
		    	case DESCRIPTION:
		    	    whatToInsert = t.getDescription();
		    	    break;
		    	case DUE_DATE:
		    	    whatToInsert = "" + t.getDueDate();
		    	    break;
		    	case ID:
		    	    whatToInsert = "" + t.getId();
		    	    break;
		    	case LATITUDE:
		    	    double latitude = t.getLatitude();
		    	    
		    	    if (latitude == Task.NO_LOCATION) {
		    		whatToInsert = NO_LOCATION_MESSAGE;
		    	    } else {
		    		whatToInsert = "" + latitude;
		    	    }
		    		
		    	    break;
		    	case LONGITUDE:
		    	    double longitude = t.getLongitude();
		    	    
		    	    if (longitude == Task.NO_LOCATION) {
		    		whatToInsert = NO_LOCATION_MESSAGE;
		    	    } else {
		    		whatToInsert = "" + longitude;
		    	    }
		    		
		    	    break;
		    	case NAME:
		    	    whatToInsert = t.getName();
		    	    break;
		    	case RECURRENCE:
		    	    whatToInsert = "" + t.getRecurrence();
		    	    break;
		    	case COMPLETION_STATUS:
		    	    boolean completed = t.isCompleted();
		    	    
		    	    if (completed) {
		    		whatToInsert = COMPLETED_MESSAGE;
		    	    } else {
		    		whatToInsert = NOT_COMPLETED_MESSAGE;
		    	    }
		    	    
		    	    break;
		    } //end attribute case/switch block

		    data[i++] = whatToInsert;
	    }
	}
	
	return data;
    }

    /**
     * 
     * @return an Object array of all the tasks for the current user
     */
    public Object[] toArray() {
    	/* A set of entries (which are key-value pairs) in which each key is a category 
    	 * name and its corresponding value is the Map that contains the Tasks in that 
    	 * category */
    	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();
    	Map<Long, Task> ret = new HashMap<Long, Task>();
    	
    	/* Loop through the Entries (K-V pairs of CategoryNames and Maps containing 
    	 * Tasks) */
    	for (Entry<String, Map<Long, Task>> entry: categoryEntries) {
    	    Map<Long, Task> taskMap = entry.getValue();
    	    ret.putAll(taskMap);
    	}
    	return ret.values().toArray();
    }
    
    /**
     * Generates a 2D array representation of this TaskList in which the rows
     * contain the Tasks in the list and the columns contain the specified 
     * attributes.
     *
     * @param attributes the attributes to include in the 2D array
     * @return the generated 2D array
     */
    public String[][] to2DArray(TaskAttribute[] attributes) { 
	String[][] table = new String[this.size()][attributes.length];
	
	/* A set of entries (which are key-value pairs) in which each key is a category 
	 * name and its corresponding value is the Map that contains the Tasks in that 
	 * category */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();
	
	/* Variables to loop through the table */
	int row = 0;
	int col = 0;
	
	/* Loop through the Entries (K-V pairs of CategoryNames and Maps containing 
	 * Tasks) */
	for (Entry<String, Map<Long, Task>> entry: categoryEntries) {
	    
	    String categoryName = entry.getKey();
	    
	    /* Get the Tasks in this Entry */
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();
	    
	    /* Loop through the Tasks in this category Map */
	    for (Task t : tasksInThisCategory) {
		
		/* Loop through the attributes of this Task that need
		 * to be included in the table. */
		for (TaskAttribute attribute : attributes) {
		    String whatToInsert = "";

		    switch(attribute) {
		    	case CATEGORY:
		    	    whatToInsert = categoryName;
		    	    break;
		    	case DESCRIPTION:
		    	    whatToInsert = t.getDescription();
		    	    break;
		    	case DUE_DATE:
		    	    whatToInsert = "" + t.getDueDate();
		    	    break;
		    	case ID:
		    	    whatToInsert = "" + t.getId();
		    	    break;
		    	case LATITUDE:
		    	    double latitude = t.getLatitude();
		    	    
		    	    if (latitude == Task.NO_LOCATION) {
		    		whatToInsert = NO_LOCATION_MESSAGE;
		    	    } else {
		    		whatToInsert = "" + latitude;
		    	    }
		    		
		    	    break;
		    	case LONGITUDE:
		    	    double longitude = t.getLongitude();
		    	    
		    	    if (longitude == Task.NO_LOCATION) {
		    		whatToInsert = NO_LOCATION_MESSAGE;
		    	    } else {
		    		whatToInsert = "" + longitude;
		    	    }
		    		
		    	    break;
		    	case NAME:
		    	    whatToInsert = t.getName();
		    	    break;
		    	case RECURRENCE:
		    	    whatToInsert = "" + t.getRecurrence();
		    	    break;
		    	case COMPLETION_STATUS:
		    	    boolean completed = t.isCompleted();
		    	    
		    	    if (completed) {
		    		whatToInsert = COMPLETED_MESSAGE;
		    	    } else {
		    		whatToInsert = NOT_COMPLETED_MESSAGE;
		    	    }
		    	    
		    	    break;
		    } //end attribute case/switch block
		    
		    /* Insert the information into the table */
		    table[row][col++] = whatToInsert;
		    
		} //end loop through attributes

		/* Move on to the next row for the next Task */
		row++;
		col = 0;
		
	    } //end loop through Tasks
	    
	} //end loop through entries
	
	return table;
    }

    /**
     * Determines whether the specified object is equal to this TaskList.  Two
     * TaskLists are equal if they contain the same categories and have the same
     * Tasks within those categories.  Formally, two categories are equal if
     * category1.equals(category2) returns true, and two Tasks are equal if
     * task1.equals(task2).
     *
     * @param other the object to be compared for equality with this TaskList
     * @return true if the specified object is equal to this TaskList; false otherwise
     */
    public boolean equals(Object other) {
        if (other == null)
            return false;
        
        if (this.getClass() != other.getClass())
            return false;
        
        TaskList otherTaskList = (TaskList)other;
        
        if (!this.categoryMaps.equals(otherTaskList.categoryMaps))
            return false;
        
        return true;
    }
    
    /**
     * Returns a String representation of this TaskList of the form
     * 
     * "Category Name: 
     * 		Name of Task 1
     * 		Name of Task 2
     * 		...
     * 
     * Category Name:
     * 		Name of Task 1
     * 		Name of Task 2
     * 		...
     * 
     * ..."
     *
     * @return a String representation of this TaskList in the above
     * 		form
     */
    public String toString() {
        StringBuffer stringRep = new StringBuffer();
        
	/* Entries are K-V pairs - the keys are the category names and the 
	 * values are the Maps containing the Tasks in those categories. */
	Set<Entry<String, Map<Long, Task>>> categoryEntries = categoryMaps.entrySet();

	for (Entry<String, Map<Long, Task>> entry : categoryEntries) {
	    /* Append this category's name */
	    stringRep.append(entry.getKey() + ":\n");
	    
	    Map<Long, Task> taskMap = entry.getValue();
	    Collection<Task> tasksInThisCategory = taskMap.values();
	    
	    for (Task t : tasksInThisCategory) {
		/* Append this Task's name */
		stringRep.append("\t" + t.getName() + "\n");
	    }
	    
	    stringRep.append("\n");
	}
        
        return stringRep.toString();
    }
    
    public Iterator<Task> iterator() {
	return new TaskListIterator();
    }
    
    
    /**
     * An iterator that allows the Tasks in this TaskList to be
     * iterated through.
     */
    private class TaskListIterator implements Iterator<Task> {
	/* Two separate arrays are maintained: one containing the Maps
	 * that represent categories, and another that holds the Tasks
	 * in a particular category.  To perform the iteration, the
	 * first category's Tasks are inserted into the array that holds
	 * Tasks, and then iterated over.  When there are no more Tasks
	 * in that category to iterate over, the next category's Tasks
	 * are inserted into the Task array. This process continues
	 * until every category's Tasks have been iterated over. */
	
	/* Stores the Maps (Map<Long, Task>) that represent categories */
	private Map<Long, Task>[] categoryArr;
	
	/* Stores the Tasks in a particular category */
	private Task[] taskArr;

	private int categoryArrIndex;
	private int taskArrIndex;

	/**
	 * Constructs a new TaskListIterator.
	 */
	public TaskListIterator() {
	    categoryArrIndex = 0;
	    taskArrIndex = 0;

	    HashMap<Long, Task>[] hashMapArray = new HashMap[0];	    
	    categoryArr = (Map<Long, Task>[]) categoryMaps.values().toArray(hashMapArray);
	}

	
	/**
	 * Returns true if there is another Task to iterate over.
	 * 
	 * @return true if there is another Task to iterate over;
	 * 		false otherwise
	 */
	public boolean hasNext() {
	    if (categoryArr.length == 0) {
		return false;
	    }

	    //Check if there are any Tasks remaining in this category
	    if (taskArr != null && (taskArrIndex < taskArr.length)) {
		return true;
	    }

	    moveToNextNonEmptyCategory();
	    //Check if the next nonempty category contains any Tasks
	    if (taskArrIndex < taskArr.length) {
		return true;
	    }
	    
	    return false;
	}

	/** 
	 * Returns the next Task and advances the iterator.
	 */
	public Task next() {
	    return taskArr[taskArrIndex++];
	}
	
	
	/**
	 * Iterates through the category Maps until a nonempty category
	 * is found.
	 * 
	 * If a nonempty category is found, the Task array is updated to
	 * contain the nonempty category's Tasks, and taskArrIndex is
	 * set to 0.
	 * 
	 * If this method is called after the last category has already
	 * been completely iterated over, nothing happens.
	 * 
	 * If no nonempty category is found, the Task array is updated
	 * to contain the Tasks in the last category (since that
	 * category contains no Tasks, the Task array is empty).  
	 * taskArrIndex is not changed in this case.
	 */
	private void moveToNextNonEmptyCategory() {
	    while(categoryArrIndex < categoryArr.length) {
		taskArr = categoryArr[categoryArrIndex++].values().toArray(new Task[0]);

		if (taskArr.length > 0) {
		    taskArrIndex = 0;
		    break;
		}
	    }
	}

	/**
	 * This operation is not supported.  It does nothing.
	 */
	public void remove() {
	    
	}
	
    } //end TaskListIterator
    
    private static void print2DArray(String[][] arr) {
	int totalRows = arr.length;
	int totalCols = arr[0].length;
	
	for (int r = 0; r < totalRows; r++) {
	    for (int c = 0; c < totalCols; c++) {
		System.out.print(arr[r][c] + " ");
	    }
	    
	    System.out.println();
	}
    }
    
    /**
     * Tests some of the methods in this class.
     *
     * @param args not used
     */
    public static void main(String[] args) {
        String name = "Do laundry";
        String description = "Don't forget to use soap.";
        Date dueDate = new Date(System.currentTimeMillis() + 5000);
        double latitude = Task.NO_LOCATION;
        double longitude = Task.NO_LOCATION;
        Recurrence recurrence = Recurrence.ONCE;
        
        //Task t1 = new Task(name, description, dueDate, latitude, longitude, recurrence);
	
	TaskList list = new TaskList();
	
	for (int i = 0; i < 11; i++) {
	    String categoryName = "Home " + i;
	    
	    list.addCategory(categoryName);
	    //list.add(categoryName, t1);
	}
	
	String[][] table = list.to2DArray(new TaskAttribute[] {TaskAttribute.ID, TaskAttribute.CATEGORY, TaskAttribute.NAME});
	
	print2DArray(table);
	
	Object[] categories = list.getCategories();
	
	System.out.println(categories[0]);
    }
}